<?php defined('BASEPATH') OR exit('No direct script access allowed');

!defined('TIMESTAMP')  && define('TIMESTAMP', time());
!defined('TIMEOFFSET') && define('TIMEOFFSET', 8*3600);

class cron_cron_model extends CI_Model
{
    public $_table;

    public function __construct()
    {
        parent::__construct();
        $this->_table = $this->db->dbprefix("common_cron");
        $this->_table_cache = $this->db->dbprefix("common_cron_cache");
        $this->_table_process = $this->db->dbprefix("common_process");
    }
    function add_row($data){
        if(is_array($data)){
            if($this->db->insert($this->_table,$data)){
                return $this->db->insert_id();
            }else{
                return false;
            }
        }
    }

    /**
     *
     * 获取单条记录
     * @param array $where 查询条件
     */
    function get_row($where=array(),$field='*'){
        $this->db->select($field);
        $query = $this->db->get_where($this->_table,$where);
        return $query->row_array();
    }

    /**
     *
     * 删除数据
     * @param array $where 删除条件
     */
    function remove_row($where=array()){
        $this->db->where($where);
        return $this->db->delete($this->_table);
    }
    public function fetch_nextrun($timestamp) {
        $timestamp = intval($timestamp);
        $query = $this->db->query("SELECT * FROM $this->_table WHERE available>'0' AND nextrun<='$timestamp' ORDER BY nextrun LIMIT 1");
        return $query->row_array();
    }

    public function fetch_nextcron() {
        $query = $this->db->query('SELECT * FROM '.$this->_table." WHERE available>'0' ORDER BY nextrun LIMIT 1");
        return $query->row_array();
    }

    public function get_cronid_by_filename($filename) {
        $query = $this->db->query('SELECT cronid FROM '.$this->_table." WHERE filename='$filename'");
        $row = $query->row_array();
        return $row['cronid'];
    }

    public function run($cronid = 0) {
        $cron = $cronid ? $this->get_row(array($cronid)) : $this->fetch_nextrun(TIMESTAMP);

        $processname ='CRON_'.(empty($cron) ? 'CHECKER' : $cron['cronid']);

        if($cronid && !empty($cron)) {
           $this->unlock($processname);
        }

        if($this->islocked($processname, 600)) {
            return false;
        }

        if($cron) {

            $cron['filename'] = str_replace(array('..', '/', '\\'), '', $cron['filename']);
            $cronfile = APPPATH.'modules/cron/crons/'.$cron['filename'];

            if($cronfile) {
                $cron['minute'] = explode("\t", $cron['minute']);
                $this->setnextime($cron);

                @set_time_limit(0);
                @ignore_user_abort(TRUE);

                if(!include $cronfile) {
                    return false;
                }
            }

        }

        $this->nextcron();
        $this->unlock($processname);
        return true;
    }

    private function nextcron() {
        $cron = $this->fetch_nextcron();
        $this->load->driver('cache');
        if(!$cron['nextrun']){
            $cron['nextrun'] = TIMESTAMP + 80000;
        }

        $this->cache->file->save('cronnextrun', $cron['nextrun'], 86400);

        return true;
    }

    private function setnextime($cron) {

        if(empty($cron)) return FALSE;

        list($yearnow, $monthnow, $daynow, $weekdaynow, $hournow, $minutenow) = explode('-', gmdate('Y-m-d-w-H-i', TIMESTAMP + TIMEOFFSET));

        if($cron['weekday'] == -1) {
            if($cron['day'] == -1) {
                $firstday = $daynow;
                $secondday = $daynow + 1;
            } else {
                $firstday = $cron['day'];
                $secondday = $cron['day'] + gmdate('t', TIMESTAMP + TIMEOFFSET);
            }
        } else {
            $firstday = $daynow + ($cron['weekday'] - $weekdaynow);
            $secondday = $firstday + 7;
        }

        if($firstday < $daynow) {
            $firstday = $secondday;
        }

        if($firstday == $daynow) {
            $todaytime = $this->todaynextrun($cron);
            if($todaytime['hour'] == -1 && $todaytime['minute'] == -1) {
                $cron['day'] = $secondday;
                $nexttime = $this->todaynextrun($cron, 0, -1);
                $cron['hour'] = $nexttime['hour'];
                $cron['minute'] = $nexttime['minute'];
            } else {
                $cron['day'] = $firstday;
                $cron['hour'] = $todaytime['hour'];
                $cron['minute'] = $todaytime['minute'];
            }
        } else {
            $cron['day'] = $firstday;
            $nexttime = $this->todaynextrun($cron, 0, -1);
            $cron['hour'] = $nexttime['hour'];
            $cron['minute'] = $nexttime['minute'];
        }

        $nextrun = @gmmktime($cron['hour'], $cron['minute'] > 0 ? $cron['minute'] : 0, 0, $monthnow, $cron['day'], $yearnow) - TIMEOFFSET;
        $data = array('lastrun' => TIMESTAMP, 'nextrun' => $nextrun);
        if(!($nextrun > TIMESTAMP)) {
            $data['available'] = '0';
        }
        $this->db->update($this->_table, $data, array('cronid' => $cron['cronid']));

        return true;
    }

    private function todaynextrun($cron, $hour = -2, $minute = -2) {

        $hour = $hour == -2 ? gmdate('H', TIMESTAMP + TIMEOFFSET) : $hour;
        $minute = $minute == -2 ? gmdate('i', TIMESTAMP + TIMEOFFSET) : $minute;

        $nexttime = array();
        if($cron['hour'] == -1 && !$cron['minute']) {
            $nexttime['hour'] = $hour;
            $nexttime['minute'] = $minute + 1;
        } elseif($cron['hour'] == -1 && $cron['minute'] != '') {
            $nexttime['hour'] = $hour;
            if(($nextminute = $this->nextminute($cron['minute'], $minute)) === false) {
                ++$nexttime['hour'];
                $nextminute = $cron['minute'][0];
            }
            $nexttime['minute'] = $nextminute;
        } elseif($cron['hour'] != -1 && $cron['minute'] == '') {
            if($cron['hour'] < $hour) {
                $nexttime['hour'] = $nexttime['minute'] = -1;
            } elseif($cron['hour'] == $hour) {
                $nexttime['hour'] = $cron['hour'];
                $nexttime['minute'] = $minute + 1;
            } else {
                $nexttime['hour'] = $cron['hour'];
                $nexttime['minute'] = 0;
            }
        } elseif($cron['hour'] != -1 && $cron['minute'] != '') {
            $nextminute = $this->nextminute($cron['minute'], $minute);
            if($cron['hour'] < $hour || ($cron['hour'] == $hour && $nextminute === false)) {
                $nexttime['hour'] = -1;
                $nexttime['minute'] = -1;
            } else {
                $nexttime['hour'] = $cron['hour'];
                $nexttime['minute'] = $nextminute;
            }
        }

        return $nexttime;
    }

    private function nextminute($nextminutes, $minutenow) {
        foreach($nextminutes as $nextminute) {
            if($nextminute > $minutenow) {
                return $nextminute;
            }
        }
        return false;
    }


    /*process*/
    public function islocked($process, $ttl = 0) {
        $ttl = $ttl < 1 ? 600 : intval($ttl);
        return $this->_status('get', $process) || $this->_find($process, $ttl);
    }

    public function unlock($process) {
        $this->_status('rm', $process);
        $this->_cmd('rm', $process);
    }

    private function _status($action, $process) {
        static $plist = array();
        switch ($action) {
            case 'set' : $plist[$process] = true; break;
            case 'get' : return !empty($plist[$process]); break;
            case 'rm' : $plist[$process] = null; break;
            case 'clear' : $plist = array(); break;
        }
        return true;
    }

    private function _find($name, $ttl) {

        if(!$this->_cmd('get', $name)) {
            $this->_cmd('set', $name, $ttl);
            $ret = false;
        } else {
            $ret = true;
        }
        $this->_status('set', $name);
        return $ret;
    }

    private function _cmd($cmd, $name, $ttl = 0) {
        return $this->_process_cmd_db($cmd, $name, $ttl);
    }

    private function _process_cmd_db($cmd, $name, $ttl = 0) {
        $ret = '';
        switch ($cmd) {
            case 'set':
                $ret = $this->db->insert($this->_table_process, array('processid' => $name, 'expiry' => time() + $ttl));
                break;
            case 'get':
                $query = $this->db->get_where($this->_table_process, array('processid' => $name), 1);
                $ret = $query->row_array();
                if(empty($ret) || $ret['expiry'] < time()) {
                    $ret = false;
                } else {
                    $ret = true;
                }
                break;
            case 'rm':
                $this->db->query("DELETE FROM $this->_table_process WHERE processid='$name' OR expiry<".intval(time()));
                break;
        }
        return $ret;
    }
}